package bigoo.peer.ride;

import static java.util.logging.Logger.GLOBAL_LOGGER_NAME;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;

import bigoo.common.PeerInfo;
import bigoo.peer.PeerFailureInterested;
import bigoo.peer.ride.Request.RequestState;

public class RequestManager implements PeerFailureInterested {

    private static final String REQUEST_ID_SEPARATOR = ":";
    private static final long TIMEOUT = 3000;

    public static String getRequestID(String rideID, String peerID) {
	return peerID + REQUEST_ID_SEPARATOR + rideID;
    }

    private Logger logger = Logger.getLogger(GLOBAL_LOGGER_NAME);
    // mapa com requests meus por caronas alheias (rideIDxrequest)
    private Map<String, Request> myRequests = new HashMap<String, Request>();
    // mapa com os request por caronas minhas (rideIDxrequest)
    private Map<String, Request> othersRequests = new HashMap<String, Request>();
    private List<Request> confirmedRequests = new LinkedList<Request>();
    private List<Request> transactions = new LinkedList<Request>();
    private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private final Lock monitor = new ReentrantLock();
    private final Condition waitPoint = monitor.newCondition();

    public void openTransaction(Request request) {
	this.monitor.lock();
	if (!this.transactions.contains(request)) {
	    logger.fine("Opening transaction for request " + request.getRequestID());
	    this.transactions.add(request);
	    this.scheduler.schedule(new TransactionTimer(request), TIMEOUT, TimeUnit.MILLISECONDS);
	} else {
	    try {
		RequestState currentState = request.getRequestState();
		this.waitPoint.await();
		if (currentState == request.getRequestState()) {
		    System.out.println(currentState);
		    this.openTransaction(request);
		}   
	    } catch (InterruptedException e) {
		e.printStackTrace(); // TODO what to do here?!
	    }
	}
	this.monitor.unlock();
    }

    public void closeTransaction(Request request) {
	this.monitor.lock();
	if (this.transactions.contains(request)) {
	    logger.fine("Closing transaction for request " + request);
	    this.transactions.remove(request);
	    this.waitPoint.signalAll();
	}
	this.monitor.unlock();
    }

    /**
     * Register a request for a ride provide by a foreign peer.
     * 
     * @param ride
     *            the ride being requested
     */
    public void registerMyRequest(Ride ride, String myPeerID) {
	logger.info("Registering request for ride " + ride.getRideID());
	PeerInfo peer = new PeerInfo(ride.getPeerID(), ride.getPeerAddress(), ride.getPeerPort());
	String requestID = getRequestID(ride.getRideID(), myPeerID);
	Request req = new Request(requestID, ride.getDestination(), ride.getTimestamp(), ride.getRideID(), peer);
	req.setRequestState(RequestState.DISCONNECTED);
	this.myRequests.put(requestID, req);
	logger.fine("Request for ride " + req.getRideID() + " has requestID " + req.getRequestID());
    }

    /**
     * Registers a request, from a foreign peer, for a ride provided by this
     * peer.
     * 
     * @param ride
     *            the ride being requested
     * @param peerID
     *            The Requester's peer id.
     */
    public String registerOtherRequest(Ride ride, PeerInfo peer) {
	String requestID = getRequestID(ride.getRideID(), peer.getPeerID());
	Request req = new Request(requestID, ride.getDestination(), ride.getTimestamp(), ride.getRideID(), peer);
	req.setRequestState(RequestState.OPEN);
	this.othersRequests.put(requestID, req);
	logger.fine("Request " + req.getRequestID() + " state is OPEN");
	return requestID;
    }

    public Request getRequest(String requestID) {
	return this.myRequests.containsKey(requestID) ? this.myRequests.get(requestID) : this.othersRequests.get(requestID);
    }

    public Collection<Request> getDisconnectedRequests() {
	Set<Request> disconnected = new HashSet<Request>();
	for (Request req : this.myRequests.values()) {
	    if (req.getRequestState() == RequestState.DISCONNECTED) {
		disconnected.add(req);
	    }
	}
	logger.fine("There's " + disconnected.size() + " requests wating to connect!");
	return disconnected;
    }

    public boolean changeRequestToNextState(Request req) throws NoTransactionException {
	this.monitor.lock();
	try {
	    if (this.transactions.contains(req)) {
		switch (req.getRequestState()) {
		    case OPEN:
			logger.info("request " + req.getRequestID() + " state changed to ACCEPTED.");
			req.setRequestState(RequestState.ACCEPTED);
			break;
		    case ACCEPTED:
			logger.info("request " + req.getRequestID() + " state changed to WAITING.");
			req.setRequestState(RequestState.WAITING);
			break;
		    case WAITING:
			if (getRequest(req.getRequestID()) != null) {
			    logger.info("request " + req.getRequestID() + " state changed to CONFIRMED.");
			    req.setRequestState(RequestState.CONFIRMED);
			    this.confirmedRequests.add(req);
			    this.pruneRequests(req);
			    this.closeTransaction(req);
			} else {
			    return false;
			}
			break;
		}
		return true;
	    } else {
		throw new NoTransactionException("There's no transaction for request " + req.getRequestID());
	    }
	} finally {
	    this.monitor.unlock();
	}
    }

    public void openRequest(Request request) {
	logger.info("request " + request.getRequestID() + " state changed to OPEN.");
	request.setRequestState(RequestState.OPEN);
    }

    public void requestRejected(Request request) {
	this.myRequests.remove(request.getRequestID());
    }

    public Collection<Request> getMyRequests() {
	return this.myRequests.values();
    }

    public Collection<Request> getOthersRequests() {
	return this.othersRequests.values();
    }

    public Collection<Request> getConfirmedRequests() {
	return this.confirmedRequests;
    }

    @Override
    public void peerFailed(String peerId) {
	logger.fine("Remote peer \'" + peerId + "\' has failed.");
	adjustStateMyRequests(peerId);
	removeOthersRequests(peerId);
    }

    private void adjustStateMyRequests(String peerId) {
	for (Map.Entry<String, Request> entry : myRequests.entrySet()) {
	    Request req = entry.getValue();
	    if (req.getPeer().equals(peerId) && req.getRequestState() != RequestState.CONFIRMED) {
		logger.info("Reseting request " + req.getRequestID() + " to state DISCONNECTED.");
		req.setRequestState(RequestState.DISCONNECTED);
	    }
	}
    }

    private void removeOthersRequests(String peerId) {
	Set<String> toRemove = new HashSet<String>();
	for (Map.Entry<String, Request> entry : this.othersRequests.entrySet()) {
	    Request req = entry.getValue();
	    if (req.getPeer().equals(peerId) && req.getRequestState() != RequestState.CONFIRMED) {
		toRemove.add(entry.getKey());
	    }
	}
	for (String requestID : toRemove) {
	    logger.info("Removing request " + requestID);
	    this.othersRequests.remove(requestID);
	}
    }

    private void pruneRequests(Request req) {
	Set<String> toRemove = new HashSet<String>();
	for (Map.Entry<String, Request> entry : this.myRequests.entrySet()) {
	    Request currentReq = entry.getValue();
	    if (currentReq.equals(req) && currentReq != req) {
		currentReq.setRequestState(RequestState.DISCONNECTED);
		toRemove.add(entry.getKey());
	    }
	}
	for (String requestID : toRemove) {
	    logger.info("Pruning request to same ride: " + requestID);
	    this.myRequests.remove(requestID);
	}

    }

    private class TransactionTimer implements Runnable {
	private Request request;

	public TransactionTimer(Request request) {
	    this.request = request;
	}

	@Override
	public void run() {
	    monitor.lock();
	    if (transactions.contains(request)) {
		logger.fine("Transaction timeout for request " + request.getRequestID());
		if (request.getRequestState() != RequestState.CONFIRMED) {
		    logger.info("Reseting request " + request.getRequestID() + " to state OPEN.");
		    request.setRequestState(RequestState.OPEN);
		}
		closeTransaction(request);
	    }
	    monitor.unlock();

	}
    }
}
